﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using MergeTool.ClassLib.Common;
using MergeTool.ClassLib.Configuration;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.VersionControl.Common;
using Microsoft.TeamFoundation.WorkItemTracking.Client;

namespace MergeTool.ClassLib
{


    public class MergeManager
    {
        private readonly Settings _settings;
        private readonly TfsConnection _connection;

        private const string FromPathPatternTemplate = "<from>";
        private const string FromPathRegexGroup = "MergeFrom";

        private const string ToPathPatternTemplate = "<to>";
        private const string ToPathRegexGroup = "MergeTo";

        private const string ChangesetIdPatternTemplate = "<changesetId>";
        private const string ChangesetIdRegexGroup = "ChangesetId";

        private const string CommentPatternTemplate = "<comment>";
        private const string CommentRegexGroup = "Comment";

        private const string RegularExpression = "(?<{0}>.{{0,}})";

        public MergeManager()
        {
            this._settings = Settings.GetInstance();
            this._connection = TfsConnection.GetInstance();
        }

        public MergeCandidate[] ParseMultipleChangesetsTextBox(string parserInput, MergeCandidate[] mergeCandidates)
        {
            string tmpParserInput = parserInput;
            int[] splitResult;
            try
            {
                splitResult = Array.ConvertAll(Regex.Split(tmpParserInput, @"[\s,;]+").Where(s => !string.IsNullOrEmpty(s)).ToArray(), int.Parse);
            }
            catch (Exception)
            {
                throw new Exception("Input >> " + parserInput + " << could not be parsed as an array of changesets, delimiter can be any whitespace character as well as colon or semicolon. ");
            }

            if (splitResult.Length < 1)
            {
                return mergeCandidates;
            }
            

            var checkChangesets = from sR in splitResult
            where !(from mC in mergeCandidates select mC.Changeset.ChangesetId).Contains(sR)
            select sR;

            if (checkChangesets.Any() && checkChangesets.Count() != splitResult.Length)
            {
                throw new Exception("Following changesets are not merge candidates for specified branches: " + string.Join(", ", checkChangesets) + ". ");
            }

            var result = from mC in mergeCandidates where splitResult.Contains(mC.Changeset.ChangesetId) 
                         orderby mC.Changeset.ChangesetId select mC;
            
            return result.ToArray();
            
        }

        public MergeCandidate[] GetMergeCandidates(string fromBranch, string toBranch, MergeOptionsEx mergeOptions, int changesetIdToStartWith)
        {
            MergeCandidate[] candidatesResult;
            if (!mergeOptions.Equals(MergeOptionsEx.Conservative))
            {
                try
                {
                    candidatesResult =_connection.GetVersionControlServer()
                        .GetMergeCandidates(new ItemSpec(fromBranch, RecursionType.Full), toBranch, mergeOptions);
                }
                catch (Exception)
                {
                    candidatesResult = _connection.GetVersionControlServer().GetMergeCandidates(fromBranch, toBranch, RecursionType.Full);
                }
            }
            else
            {
                candidatesResult = _connection.GetVersionControlServer().GetMergeCandidates(fromBranch, toBranch, RecursionType.Full);
            }

            if (_settings.MergeLoggedUserChangesets)
            {
                return candidatesResult.Where(cRes => cRes.Changeset.Owner == _settings.ConnectionInfo.SelectedUserName).ToArray();
            }

            return candidatesResult.Where(cRes => cRes.Changeset.ChangesetId > changesetIdToStartWith).ToArray();
        }

        public string GetBranchFinalCommentFromPattern(string branchCheckinCommentPattern, string fromPath, string toPath)
        {
            string finalComment;
            try
            {
                if (_settings.BranchNameOnlyInComments)
                {
                    fromPath = fromPath.Substring(fromPath.LastIndexOf('/') + 1);
                    toPath = toPath.Substring(toPath.LastIndexOf('/') + 1);
                }
                if (string.IsNullOrEmpty(branchCheckinCommentPattern))
                {
                    if (string.IsNullOrEmpty(_settings.DefaultBranchCheckinCommentPattern))
                        throw new Exception("Check-in comment pattern is empty for branches as well as default value.");

                    branchCheckinCommentPattern = _settings.DefaultBranchCheckinCommentPattern;
                }
                finalComment = branchCheckinCommentPattern.Replace(FromPathPatternTemplate, fromPath);
                finalComment = finalComment.Replace(ToPathPatternTemplate, toPath);
            }
            catch (Exception ex)
            {
                throw new Exception("Error occured while generating comment from branch pattern! info:" + ex.Message);
            }
            return finalComment;
        }

        public string GetChangesetFinalCommentFromPattern(string changesetCheckinCommentPattern, string fromPath, string toPath, string changesetId, string changesetComment)
        {
            string finalComment;
            try
            {
                if (_settings.BranchNameOnlyInComments)
                {
                    fromPath = fromPath.Substring(fromPath.LastIndexOf('/') + 1);
                    toPath = toPath.Substring(toPath.LastIndexOf('/') + 1);
                }

                if (string.IsNullOrEmpty(changesetCheckinCommentPattern))
                {
                    if(string.IsNullOrEmpty(_settings.DefaultChangesetCheckinCommentPattern))
                        throw new Exception("Check-in comment pattern is empty for changesets as well as default value.");

                    changesetCheckinCommentPattern = _settings.DefaultChangesetCheckinCommentPattern;
                }

                string tempChangesetCheckinCommentPattern = changesetCheckinCommentPattern;

                if (_settings.IsKeepMergeHistoryInComments)
                {
                    finalComment = changesetCheckinCommentPattern.Replace(FromPathPatternTemplate, fromPath);
                    finalComment = finalComment.Replace(ToPathPatternTemplate, toPath);
                    finalComment = finalComment.Replace(ChangesetIdPatternTemplate, changesetId);
                    finalComment = finalComment.Replace(CommentPatternTemplate, changesetComment);

                    //finalComment = string.Format("{0} {1}", changesetComment , finalComment);
                }
                else
                {
                    tempChangesetCheckinCommentPattern = Regex.Escape(tempChangesetCheckinCommentPattern);

                    tempChangesetCheckinCommentPattern = tempChangesetCheckinCommentPattern.Replace(FromPathPatternTemplate,
                        string.Format(RegularExpression, FromPathRegexGroup));

                    tempChangesetCheckinCommentPattern = tempChangesetCheckinCommentPattern.Replace(ToPathPatternTemplate, string.Format(RegularExpression, ToPathRegexGroup));

                    tempChangesetCheckinCommentPattern = tempChangesetCheckinCommentPattern.Replace(ChangesetIdPatternTemplate,
                        string.Format(RegularExpression, ChangesetIdRegexGroup));

                    tempChangesetCheckinCommentPattern = tempChangesetCheckinCommentPattern.Replace(CommentPatternTemplate, string.Format(RegularExpression, CommentRegexGroup));

                    Match match = Regex.Match(changesetComment, tempChangesetCheckinCommentPattern);

                    if (match.Success)
                    {
                        GroupCollection groups = match.Groups;
                        changesetComment = groups[CommentRegexGroup].Value;
                    }

                    finalComment = changesetCheckinCommentPattern.Replace(FromPathPatternTemplate,
                        fromPath);
                    finalComment = finalComment.Replace(ToPathPatternTemplate, toPath);
                    finalComment = finalComment.Replace(ChangesetIdPatternTemplate, changesetId);
                    finalComment = finalComment.Replace(CommentPatternTemplate, changesetComment);
                }

            }
            catch (Exception ex)
            {
                throw new Exception("Error occured while generating comment from changeset pattern! info: " + ex.Message);
            }

            return finalComment;
        }

        public Changeset GetChangesetDetails(int changeSetId)
        {
            return _connection.GetVersionControlServer().GetChangeset(changeSetId);
        }

        public MergeObjectLink MergeByBranch(string fromPath, string toPath, MergeOptionsEx mergeOptions, string commentPattern, int changesetIdToStartWith)
        {
            var mergeCandidates = GetMergeCandidates(fromPath, toPath, mergeOptions, changesetIdToStartWith);
            if (!mergeCandidates.Any())
            {
                return null;
            }
            int minChangesetCandidate = mergeCandidates.Select(mc => mc.Changeset).Select(cs => cs.ChangesetId).Min();
            int maxChangesetCandidate = mergeCandidates.Select(mc => mc.Changeset).Select(cs => cs.ChangesetId).Max();

            VersionSpec fromVersionSpec = VersionSpec.ParseSingleSpec("C" + minChangesetCandidate.ToString(),_settings.ConnectionInfo.SelectedUserName);
            VersionSpec toVersionSpec = VersionSpec.ParseSingleSpec("C" + maxChangesetCandidate.ToString(), _settings.ConnectionInfo.SelectedUserName);

            var tempWorkspace = new TfsTempWorkspace(_connection.GetVersionControlServer(), "MergeTool", _settings.ConnectionInfo.SelectedUserName, _settings.TempWorkspacePath);

            tempWorkspace.Map(_connection.GetTeamProject().ServerItem, _connection.GetTeamProject().Name);

            foreach (WorkingFolder wf in tempWorkspace.Workspace.Folders)
            {
                Directory.CreateDirectory(wf.LocalItem);
            }
            tempWorkspace.Workspace.Refresh();
            var statusCheck = tempWorkspace.Workspace.Merge(fromPath, toPath, fromVersionSpec, toVersionSpec, LockLevel.None, RecursionType.Full, mergeOptions);

            if (statusCheck.NumFailures > 0)
            {
                var errors = statusCheck.GetFailures().Where(fail => fail.Severity.Equals(SeverityType.Error));
                if (errors.Any())
                {
                    throw new Exception(errors.ToArray()[0].Message);
                }
            }
            List<WorkItemCheckinInfo> wicil = new List<WorkItemCheckinInfo>();
            if (_settings.DefaultWorkItemAssociation != DefaultWorkItemAssociation.Never)
            {
                var witStore = _connection.GeTfsTeamProjectCollection().GetService<WorkItemStore>();
                wicil.Add(new WorkItemCheckinInfo(witStore.GetWorkItem(_settings.DefaultWorkItemToAssociate), WorkItemCheckinAction.Associate));
            }
            

            //string comment = "Regular merge of branches: " + " [" + fromPath.Substring(fromPath.LastIndexOf('/') + 1) + " --> " + toPath.Substring(toPath.LastIndexOf('/') + 1) + "]";
            string comment = GetBranchFinalCommentFromPattern(commentPattern, fromPath, toPath);

            
            return new MergeObjectLink
            {
                Comment = comment,
                TempWorkspace = tempWorkspace,
                WorkItemsToAssociate = wicil,
                ToPath = toPath,
                FromPath = fromPath,
                ChangesetOwner = _settings.ConnectionInfo.SelectedUserName
            };
        }

        public MergeObjectLink ResolveConflictsDuringMerge(MergeObjectLink mergeObjectLink)
        {
            mergeObjectLink.InterruptMessage = null;
            CheckinEvaluationResult cer;

            cer = mergeObjectLink.TempWorkspace.Workspace.EvaluateCheckin2(CheckinEvaluationOptions.All, mergeObjectLink.TempWorkspace.Workspace.GetPendingChanges(), mergeObjectLink.Comment, null, mergeObjectLink.WorkItemsToAssociate.ToArray());

            foreach (CheckinConflict cc in cer.Conflicts)
            {
                if (!cc.Resolvable)
                {
                    throw new Exception("Unresolvable conflict has occured! Details: " + cc.Message);
                }
            }

            if (cer.Conflicts.Length != 0)
            {
                mergeObjectLink.IsConflictHasOccured = true;
                ResolveConflictsDialog rd = new ResolveConflictsDialog(mergeObjectLink.TempWorkspace.Workspace);
                rd.ShowDialog();
            }

            cer = mergeObjectLink.TempWorkspace.Workspace.EvaluateCheckin2(CheckinEvaluationOptions.All, mergeObjectLink.TempWorkspace.Workspace.GetPendingChanges(), mergeObjectLink.Comment, null, mergeObjectLink.WorkItemsToAssociate.ToArray());

            if (cer.Conflicts.Length != 0)
            {
                mergeObjectLink.InterruptMessage = "User has cancelled resolution of conflicts - skipping!";
            }

            return mergeObjectLink;
        }

        public MergeObjectLink PerformCheckIn(MergeObjectLink mergeObjectLink)
        {
            mergeObjectLink.InterruptMessage = null;

            if (_settings.IsAlwaysShowCheckinConfirmation || mergeObjectLink.IsConflictHasOccured)
            {
                CheckInDialog cid = new CheckInDialog(mergeObjectLink.TempWorkspace.Workspace, mergeObjectLink.Comment, mergeObjectLink.WorkItemsToAssociate.ToArray(), mergeObjectLink.ChangesetOwner);
                
                mergeObjectLink.ChangesetIdAfterCheckin = cid.ShowDialog();

                if (!cid.DialogResult.Equals(DialogResult.OK) && !cid.DialogResult.Equals(DialogResult.Yes))
                {
                    mergeObjectLink.InterruptMessage = "User has cancelled Check-In - skipping!";
                    return mergeObjectLink;
                }
            }
            else
            {
                var workSpace = mergeObjectLink.TempWorkspace.Workspace;
                PendingChange[] pendingChange = workSpace.GetPendingChanges();
                List<WorkItemCheckedInfo> workItemCheckedInfo = new List<WorkItemCheckedInfo>();
                
                foreach (WorkItemCheckinInfo w in mergeObjectLink.WorkItemsToAssociate)
                {
                    workItemCheckedInfo.Add(new WorkItemCheckedInfo(w.WorkItem.Id, true, w.CheckinAction));
                }

                var checkinEvaluation = mergeObjectLink.TempWorkspace.Workspace.EvaluateCheckin2(CheckinEvaluationOptions.All, mergeObjectLink.TempWorkspace.Workspace.GetPendingChanges(), mergeObjectLink.Comment, null, mergeObjectLink.WorkItemsToAssociate.ToArray());
                if (checkinEvaluation.PolicyFailures.Any())
                {
                    string policyErrorsMessage = "";
                    foreach (var policyFailure in checkinEvaluation.PolicyFailures)
                    {
                        policyErrorsMessage += policyFailure.Message + " ";
                    }
                    throw new Exception("Cannot perform silent check-in due to these policy failures: " + policyErrorsMessage);
                }

                mergeObjectLink.ChangesetIdAfterCheckin = mergeObjectLink.TempWorkspace.Workspace.CheckIn(pendingChange, mergeObjectLink.ChangesetOwner,
                mergeObjectLink.Comment,
                null,
                mergeObjectLink.WorkItemsToAssociate.ToArray(),
                null);
            }

            
            mergeObjectLink.TempWorkspace.Dispose();
            return mergeObjectLink;
        }

        public MergeObjectLink MergeByChangeset(string fromPath, string toPath, Changeset changeset, MergeOptionsEx mergeOptions, string commentPattern, int changesetIdToStartWith)
        {
            var mergeCandidates = GetMergeCandidates(fromPath, toPath, mergeOptions, changesetIdToStartWith);
            
            if (!mergeCandidates.Select(mc => mc.Changeset).Select(cs => cs.ChangesetId).Contains(changeset.ChangesetId))
            {
                throw  new Exception("Changeset: " + changeset.ChangesetId + " from " + fromPath + " to " + toPath + " is not available for merging! ");
            }
            VersionSpec versionSpec = VersionSpec.ParseSingleSpec("C" + changeset.ChangesetId.ToString(),
                _settings.ConnectionInfo.SelectedUserName);
            var tempWorkspace = new TfsTempWorkspace(_connection.GetVersionControlServer(), "MergeTool", _settings.ConnectionInfo.SelectedUserName, _settings.TempWorkspacePath);

            tempWorkspace.Map(_connection.GetTeamProject().ServerItem, _connection.GetTeamProject().Name);

            foreach (WorkingFolder wf in tempWorkspace.Workspace.Folders)
            {
                Directory.CreateDirectory(wf.LocalItem);
            }
            tempWorkspace.Workspace.Refresh();
            var statusCheck = tempWorkspace.Workspace.Merge(fromPath, toPath, versionSpec, versionSpec, LockLevel.None, RecursionType.Full, mergeOptions);

            if (statusCheck.NumFailures > 0)
            {
                var errors = statusCheck.GetFailures().Where(fail => fail.Severity.Equals(SeverityType.Error));
                if (errors.Any())
                {
                    throw new Exception(errors.ToArray()[0].Message);
                }
            }
            List<WorkItemCheckinInfo> wicil = new List<WorkItemCheckinInfo>();
            foreach (WorkItem wit in changeset.WorkItems)
            {
                wicil.Add(new WorkItemCheckinInfo(wit, WorkItemCheckinAction.Associate));
            }

            if (_settings.DefaultWorkItemAssociation == DefaultWorkItemAssociation.Always || !wicil.Any() && _settings.DefaultWorkItemAssociation
                == DefaultWorkItemAssociation.OnlyWhenNoWorkItemAssociated)
            {
                var witStore = _connection.GeTfsTeamProjectCollection().GetService<WorkItemStore>();
                wicil.Add(new WorkItemCheckinInfo(witStore.GetWorkItem(_settings.DefaultWorkItemToAssociate), WorkItemCheckinAction.Associate));
            }

            var comment = GetChangesetFinalCommentFromPattern(commentPattern, fromPath, toPath,
                changeset.ChangesetId.ToString(), changeset.Comment);

            string changesetOwner;

            if (_settings.IsKeepOriginalOwnerOfMergedChangeset)
            {
                changesetOwner = changeset.Owner;
            }
            else
            {
                changesetOwner = _settings.ConnectionInfo.SelectedUserName;
            }

            return new MergeObjectLink
            {
                Comment = comment,
                TempWorkspace = tempWorkspace,
                WorkItemsToAssociate = wicil,
                ToPath = toPath,
                FromPath = fromPath,
                ChangesetOwner = changesetOwner
            };
        }
     }

    public class ResolveConflictsDialog
    {
        public DialogResult DialogResult { get; set; }
        private Workspace workSpace = null;
        private Form resolveConflictsDialog = null;

        public ResolveConflictsDialog(Workspace ws)
        {
            workSpace = ws;
        }

        public bool ShowDialog()
        {
            workSpace.Refresh();


            Assembly controlsAssembly = Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Microsoft.TeamFoundation.VersionControl.Controls.dll"));
            Type vcResolveDialogType = controlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.DialogResolveConflicts");

            ConstructorInfo ci = vcResolveDialogType.GetConstructor(
                   BindingFlags.Instance | BindingFlags.Public,
                   null,
                   new Type[] { typeof(Workspace), typeof(string[]), typeof(bool) },
                   null);
            resolveConflictsDialog = (Form)ci.Invoke(new object[] { workSpace, new string[0], true });

            resolveConflictsDialog.ShowDialog();
            this.DialogResult = resolveConflictsDialog.DialogResult;
            Cursor.Current = Cursors.WaitCursor;
            resolveConflictsDialog.Dispose();
            return true;
        }

    }

    public class CheckInDialog
    {
        public DialogResult DialogResult { get; set; }
        private Workspace workSpace = null;
        private Form checkInDialog = null;
        private PropertyInfo checkedPendingChanges = null;
        private PropertyInfo checkedWorkItems = null;
        private PropertyInfo checkinNotes = null;
        private PropertyInfo comment = null;
        private PropertyInfo policyFailureOverrideReason = null;
        private PropertyInfo policyFailures = null;
        private string _comment;
        private List<WorkItemCheckedInfo> _wcil = new List<WorkItemCheckedInfo>();
        private string _owner;

        public CheckInDialog(Workspace ws, string comment, WorkItemCheckinInfo[] wcil, string owner)
        {
            workSpace = ws;
            _comment = comment;
            foreach (WorkItemCheckinInfo w in wcil)
            {
                _wcil.Add(new WorkItemCheckedInfo(w.WorkItem.Id, true, w.CheckinAction));
            }
            _owner = owner;
        }

        public int ShowDialog()
        {
            workSpace.Refresh();
            //Thread.Sleep(100);
            PendingChange[] pendingChange = workSpace.GetPendingChanges();
            PendingChange[] checkedinPendingChange = workSpace.GetPendingChanges();

            //string omg = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Microsoft.TeamFoundation.VersionControl.Controls.dll");
            Assembly controlsAssembly = Assembly.LoadFile(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Microsoft.TeamFoundation.VersionControl.Controls.dll"));
            Type vcCheckinDialogType = controlsAssembly.GetType("Microsoft.TeamFoundation.VersionControl.Controls.DialogCheckin");

            ConstructorInfo ci = vcCheckinDialogType.GetConstructor(
                   BindingFlags.Instance | BindingFlags.NonPublic,
                   null,
                   new Type[] { typeof(Workspace), typeof(PendingChange[]), typeof(PendingChange[]), 
               typeof(string), typeof(CheckinNote), typeof(WorkItemCheckedInfo[]), typeof(string) },
                   null);
            checkInDialog = (Form)ci.Invoke(new object[] {  workSpace, pendingChange, checkedinPendingChange, _comment, null, _wcil.ToArray(), "" });
            
            checkedPendingChanges = vcCheckinDialogType.GetProperty("CheckedChanges", BindingFlags.Instance | BindingFlags.NonPublic);
            checkedWorkItems = vcCheckinDialogType.GetProperty("CheckedWorkItems", BindingFlags.Instance | BindingFlags.NonPublic);
            checkinNotes = vcCheckinDialogType.GetProperty("CheckinNotes", BindingFlags.Instance | BindingFlags.NonPublic);
            comment = vcCheckinDialogType.GetProperty("Comment", BindingFlags.Instance | BindingFlags.NonPublic);
            policyFailureOverrideReason = vcCheckinDialogType.GetProperty("PolicyFailureOverrideReason", BindingFlags.Instance | BindingFlags.NonPublic);
            policyFailures = vcCheckinDialogType.GetProperty("PolicyFailures", BindingFlags.Instance | BindingFlags.NonPublic);

            checkInDialog.ShowDialog();
            this.DialogResult = checkInDialog.DialogResult;
            Cursor.Current = Cursors.WaitCursor;

            if (DialogResult != DialogResult.Cancel)
            {

                PendingChange[] selectedPendingChange = (PendingChange[])checkedPendingChanges.GetValue(checkInDialog, null);
                WorkItemCheckinInfo[] checkedWorkItemInfo = (WorkItemCheckinInfo[])checkedWorkItems.GetValue(checkInDialog, null);
                string comments = (string)comment.GetValue(checkInDialog, null);
                string policyReason = (string)policyFailureOverrideReason.GetValue(checkInDialog, null);
                CheckinNote notes = (CheckinNote)checkinNotes.GetValue(checkInDialog, null);
                PolicyFailure[] failures = (PolicyFailure[])policyFailures.GetValue(checkInDialog, null);

                PolicyOverrideInfo overrideinfo = new PolicyOverrideInfo(policyReason, failures);

                int changeset;
                try
                {
                    changeset = workSpace.CheckIn(selectedPendingChange,
                        _owner,
                        comments,
                        notes,
                        checkedWorkItemInfo,
                        overrideinfo);
                    return changeset;
                }
                catch (Exception exp)
                {
                    MessageBox.Show("Check in Failed due to " + exp.Message);
                    return -1;
                }
                finally
                {
                    checkInDialog.Dispose();
                }
            }
            else
            {
                checkInDialog.Dispose();
                return -1;
            }
        }

    }

    public class MergeObjectLink
    {
        public MergeObjectLink()
        {
            this.IsConflictHasOccured = false;
            this.ChangesetIdAfterCheckin = -1;
        }
        
        public string Comment { get; set; }

        public TfsTempWorkspace TempWorkspace { get; set; }

        public List<WorkItemCheckinInfo> WorkItemsToAssociate { get; set; }

        public string InterruptMessage { get; set; }

        public string ToPath { get; set; }

        public string FromPath { get; set; }

        public string ChangesetOwner { get; set; }

        public bool IsConflictHasOccured { get; set; }

        public int ChangesetIdAfterCheckin { get; set; }
    }
}
